#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>

#define ERRDIFF_MAJOR 200
#define ERRDIFF_NAME "errdiff"

/* define physical address of registers */
#define  ACTIVE_PADDR  0x40000000
#define  DONE_PADDR    0x40000004
#define  START_PADDR   0x40000008
#define  END_PADDR     0x4000000c

/* mapped register virtual address pointer */
static void __iomem *active_vaddr;
static void __iomem *done_vaddr;
static void __iomem *start_vaddr;
static void __iomem *end_vaddr;

#define ACTIVE 0
#define DONE 1
#define START 2
#define END 3

struct trandata{
  u32 number;
  u32 data;
};

/*
 * @description: open device
 * @param-inode: The inode passed to the driver
 * @param-filp : device file. The file structure has a member variable called private_data, which
 *               is usually pointed to the device structure when open.
 * @return     : 0 success; other failure
 * */
static int errdiff_open(struct inode *inode, struct file *filp)
{
  printk("errdiff driver opened!\r\n");
  return 0;
}

/*
 * @description: read from device
 * @param-filp : device file to be opened (file descriptor)
 * @param-buf  : data buffer returned to user space
 * @param-cnt  : length of the data to be read
 * @param-offt : offset relative to the file header address
 * @return     : number of bytes read, if negative, indicates a read failure
 * */
static ssize_t errdiff_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
  struct trandata regdes;
  int retvalue = 0;

  if (cnt != 8) {
    printk(KERN_ERR "Number of reading data is not 8!\r\n");
    return -EFAULT;
  }

  retvalue = copy_from_user(&regdes, buf, 8);
  if (0 > retvalue) {
    printk(KERN_ERR "Kernel read failed!\r\n");
    return -EFAULT;
  }

  switch (regdes.number) {
    case ACTIVE: regdes.data = readl(active_vaddr); break;
    case DONE: regdes.data = readl(done_vaddr); break;
    case START: regdes.data = readl(start_vaddr); break;
    case END: regdes.data = readl(end_vaddr); break;
    default: printk(KERN_ERR "No reg number %u(0~3)!\r\n", regdes.number); return -EFAULT;
  }

  retvalue = copy_to_user(buf+4, &regdes.data, 4);

  if (0 > retvalue) {
    printk(KERN_ERR "Kernel read failed!\r\n");
    return -EFAULT;
  }

  // printk("errdiff driver read!\r\n");
  return cnt;
}

/*
 * @description: write into device
 * @param-filp : device file to be opened (file descriptor)
 * @param-buf  : data to be written into device
 * @param-cnt  : length of data to be written
 * @param-offt : offset relative to file header address
 * @return     : number of bytes written, if negative, indicates a write failure
 * */
static ssize_t errdiff_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) {
  struct trandata regdes;
  int retvalue = 0;

  if (cnt != 8) {
    printk(KERN_ERR "Number of writing data is not 8!\r\n");
    return -EFAULT;
  }

  retvalue = copy_from_user(&regdes, buf, cnt);
  if (0 > retvalue) {
    printk(KERN_ERR "Kernel write failed!\r\n");
    return -EFAULT;
  }

  switch(regdes.number) {
    case ACTIVE: writel(regdes.data, active_vaddr); break;
    case DONE: writel(regdes.data, done_vaddr); break;
    case START:  writel(regdes.data, start_vaddr);  break;
    case END:  writel(regdes.data, end_vaddr);  break;
    default: printk(KERN_ERR "No reg number %u!(0~3)\r\n", regdes.number); return -EFAULT;
  }

  // printk("errdiff driver write!\r\n");
  return 0;
}

/*
 * @description: close/release device
 * @param-filp : device file to be closed (file descriptor)
 * @return     : 0 success; other failure
 * */
static int errdiff_release(struct inode *inode, struct file *filp)
{
  printk("errdiff driver release!\r\n");

  return 0;
}

/*
 * device operation function structure
 * */
static struct file_operations errdiff_fops = {
  .owner   = THIS_MODULE,
  .open    = errdiff_open,
  .read    = errdiff_read,
  .write   = errdiff_write,
  .release = errdiff_release,
};

/*
 * @description: drive entrance function
 * @param      : none
 * @return     : 0 success; other failure
 * */
static int __init errdiff_init(void)
{
  int retvalue = 0;

  /* redister address mapping */
  active_vaddr = ioremap(ACTIVE_PADDR, 4);
  done_vaddr   = ioremap(DONE_PADDR, 4);
  start_vaddr  = ioremap(START_PADDR,  4);
  end_vaddr    = ioremap(END_PADDR,  4);

  // register character device driver
  retvalue = register_chrdev(ERRDIFF_MAJOR, ERRDIFF_NAME, &errdiff_fops);
  if(retvalue < 0) {
    printk("errdiff driver register failed\r\n");
    return retvalue;
  }

  printk("The errdiff driver is successfully mounted\r\n");
  return 0;
}

/*
 * @description: drive exit function
 * @param      : none
 * @return     : none
 * */
static void __exit errdiff_exit(void)
{

  // cancel character device driver
  unregister_chrdev(ERRDIFF_MAJOR, ERRDIFF_NAME);

  /* cancel memory mapping */
  iounmap(active_vaddr);
  iounmap(done_vaddr);
  iounmap(start_vaddr);
  iounmap(end_vaddr);

  printk("errdiff device exit\r\n");
}

module_init(errdiff_init);
module_exit(errdiff_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("rogerskelamen");

